"""ocfdb module

Author: Michal Mazurek

Copyright (c) 2006, IF Research Polska (www.ifresearch.pl)
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice, this list of conditions and the following disclaimer in the
   documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
   derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.



Examples of usage:
    
    from ocfdb import ocfdb
        
    # packages file format:
    # MODIFYDATE:123456789:
    # TABLE:packages:
    # id;name;version
    # 1;clamav;2.0
    # 2;dspam;3.6.4
    
    # file is packed with gzip
    # $: gzip packages
    # /\ this will output packages.gz
    
    # database initialization
    try:
        db = ocfdb('packages.gz')
    except OCFDBError:
        print "Database Error", OCFDBError
    # getting rows, this will return rows internal IDs in list, not column values nor objects    
    rowids = db.packages.getrows('int(__row.id) <= 2') # we have to remember that all columns values are strings
 
    # to get objects we have to fetch them from database:    
    rows = db.packages.getrowsbyId(rowids)
    
    #now when we have rows we can do something on them:
    
    for row in rows:
        print row.name, row.version
        
    # let's now update name column in first row    
    
    rows[0].name = "clamav"

    print rows[0].name
    # returns: clamav
    
    # so when we do any changes, we have to save database, so they won't be lost at another opening of database
    
    db.savedatabase()

"""
import os
import re
import time
import gzip

__version__ = "0.1"
#__doc__ = "ocfdb module"


class OCFDBError(Exception):
            pass

class ocfdb:
    """Main Database Class"""
    databasefile = ""
    tables = {}    

    def __init__(self, filename = ""):
        #setting database file name
        self.databasefile = filename
        #initiating tables
    
        self.tables = {}
        """
         reading tables from file
        """
        #is zip file
        if os.path.exists(filename):
            try:    
                gzip.open(filename)                
            except:
                raise OCFDBError, filename+" is not a gz file!"

        
            self.readtables();
            self.__nonzero__ = True
        elif not os.access(os.path.dirname(filename), os.W_OK):
            raise OCFDBError, os.path.dirname(filename)+" is not writable!"



    def __eq__(self, val):
        return True
    
    def __getattr__(self, name):        
                try:
                    return self.gettablebyname(name)
                except:
                    raise OCFDBError, "Database "+name+" doesn't exists"
    
    def gettablebyname(self, name):
        """method for taking tables by its names"""
        try:
            return self.tables[name]
        except:
            raise OCFDBError, "Table "+name+" doesn't exists!"
        
                
    def savedatabase(self):
            """Saves database to databasefile"""
            returnstring = ""
            t = int(time.time())
            returnstring += "MODIFYDATE:%s:\n" % t
            #parsing tables and objects to csv, and ocf database format
            #iterating through tables
            for table in self.tables.values():
                
                strp=""
                #writing table header
                strp += "TABLE:%s:\n" % table.name
                #getting table in cvs
                strp += self.gettablecsv(table)
                #writing table footer    
                #str += "ENDTABLE:"+table.name+":\n"
                returnstring += strp
            gz = gzip.open(self.databasefile, 'wb')
            gz.write(returnstring)
        
            pass    
    
    def gettablecsv(self, table):
        """Returns table in csv version"""
        if type(table) == type(" "):
            table = self.gettablebyname(table)
        
        
        if table.ischanged():
            return table.cvs
            #writing first line of table cvs which contains names
        strp = ";".join(table.colnumbers)
        strp += "\n"
        #iterating for rows
        slist = [row.returncsv() for row in table.rows]    
        strp += "\n".join(slist)
        return strp+"\n"
            
    def newtable(self, name):
        """Creates new table"""
        self.tables[name] = ocfdbtable()
        self.tables[name].name = name
        self.tables[name].notchanged()
        self.tables[name].rows = []
        self.tables[name].lastinsertid = 1
        self.tables[name].colnames = {}
        self.tables[name].colnumbers = []
        return self.tables[name]
    
    def deletetable(self, name):
        """Deletes table"""
        self.tables.__delitem__(name)
    
    def readtables(self):
        """reading tables from gzip file"""
        #zip = zipfile.ZipFile(self.databasefile, 'r')        
        gz = gzip.GzipFile(self.databasefile)
        #checking zip for index file
        
        self.txtdatabase = gz.read('index.ocfdb')
        #readinb name, and cvs from table
        s = re.split("\n(TABLE:.*?:)\n",self.txtdatabase, re.M)
        tablecount=(len(s)-1)/2
    
        s.__delitem__(0)
        #tablesstr = []
    
        rowid = 0
        tablesstr = [ s[n]+"\n"+s[n+1] for n in range(0, len(s)-1, 2) ]
    
        for tabletxt in tablesstr:
            s = re.search('^TABLE:(.*?):\n(.*)', tabletxt, re.M | re.S)
            if(s):
                #if it is found in database file 
                #naming table
                name = s.group(1)
                #creating new table object
                table = self.newtable(name)
                #reading csv to table object
                table.csv = s.group(2)                
                rows = []
                #we are splitting csv to list of lines
                rows = table.csv.splitlines()
                #first line contains names for each column
                namerow = rows[0]
                num = 0
                table.colnames = {}
                table.colnumbers = []
                #now we are splitting csv row to list of columns
                #this loop will create dict with names as keys, and position as value
                #and list of names
                for name in namerow.split(";"):
                    table.colnames[name] = num
                    table.colnumbers.append(name)
                    num +=1    
                #we are getting rid off csv row with names
                rows.__delitem__(0)
                
                rowsid=0
                
                
                #getting rows values
                for row in rows:
                    if(row != ""):
                        if table.lastinsertid > 1:
                            table.lastinsertid += 1
                        #initiating new row object
                        rowclass = ocfdbrow()
                        #writing colnames to row object
                        rowclass.colnames = table.colnames
                        rowclass.colnumbers = table.colnumbers
                        #getting row to object                        
                        rowclass.getrow(row.split(";", len(row.split(";"))-1))
                        rowclass.cvs = row
                        rowclass.setid(table.lastinsertid);
                        #appending this object to table rows list
                        table.rows.append(rowclass)
                        
                        if table.lastinsertid == 1:
                            table.lastinsertid += 1
                        
                        rowid += 1                  


class ocfdbtable:
    
    colnumbers = []
    colnames = {}
    rows = []
        
    def addcolumn(self, name):
        """Adds column to current table"""
        if len(name) == 0:
            raise OCFDBError, "You must specify column name"
        
        try:
            self.colnumbers.append(name)
            self.colnames[name] = len(self.colnumbers)-1
            
        except:
            self.colnumbers = [name]
            self.colnames = {name: 0}
        return True
    
    def notchanged(self):
        """Changes changed flag to False"""
        self.__dbtablechanged = False;    
    def ischanged(self):
        """Returns changed flag"""
        return self.__dbtablechanged    
        
    def getrows(self, wql = "", limit=0):
        """getting rows from table, 
        it's using ocfdb query language
            it is similiar to python boolean statements
            eg.:
                1.When we want to get every row with column id greater that 2>
                    __row.id > 2
                2.When we want to get every row with column text value 'second', and id less equal than 4
                    __row.text == 'second' and __row.id <= 4
                3.When we want to find rows with special regular expression type:
                    like('tex.*', __row.text)
                  function like is analogical to re.search, and with all flags"""
        limitfrom = 0
        limitto = 0
        if type(limit) == type(()):
            limitfrom = limit[0]
            limitto = limit[1]
        else:
            limitto = limit
        
        retrows = []
        retrowid = 0            
        if wql != "":
        
            for row in self.rows:
                bool = True
                wql = wql.replace("__row", "row");
                wql = wql.replace('like', 're.search')
                
                if eval(wql):
                    
                    if limitfrom > 0:
                        
                        if retrowid < limitfrom:
                                bool = False
                    if limitto > 0:
                        
                        if retrowid >= limitto:
                                bool = False
                                if retrowid > limitto:
                                    break;
                    if bool:                            
                        retrows.append(row.getid())
                    
                    retrowid += 1
                    
            
            return retrows        
        
        else:
            return False
        
    def getlastinsertid(self):
        return self.lastinsertid
    
    def getlastinsert(self, name):
        lastrow = self.rows[len(self.rows)-1]
        return lastrow.__getattr__(name)
    
    def getrowsbyId(self, ids = ""):
        
        if type(ids) == type([]):
            retrows = []
            for id in ids:                    
                    for row in self.rows:
                    #    print id
                        if int(row.getid()) == int(id):
                            retrows.append(row)            
                            break            
                    else:
                        raise OCFDBError,  "Row with id "+str(id)+" doesn't exists!"
                    
            
            return retrows
        elif ids != "":
            for row in self.rows:
                if int(row.getid()) == int(ids):
                    return row
        
    def insertrows(self, rows = ""):    
        """Inserts rows to database
        Example:
            insertrows({'colname': 'value', 'colname2': value})
          it can be also:
            listofrows = [{'colname': 'value', 'colname2': value1}, {'colname': 'value2', 'colname2': value3}] 
            insertrows(listofrows)
        """ 
        try:
            t = self.rows[0]
        except IndexError, KeyError:
            self.rows = []
        
        if type(rows) == type([]):
            for row in rows:
                if type(row) == type({}):
                    newrow = ocfdbrow()
                    newrow.cols = []
                    newrow.colsa = {}
                    for key in self.colnumbers:
                        try:            
                            rows[key] = str(rows[key])
                            rows[key] = rows[key].replace(";", "")
                            rows[key] = rows[key].replace("\n", "\\n")    
                            newrow.__dict__[key] = str(row[key])
                            newrow.colsa[key] = str(row[key])
                            newrow.cols.append(str(row[key]))
                        except:
                            newrow.__dict__[key] = ''
                            newrow.colsa[key] = ''
                            newrow.cols.append('')
                            
                    self.lastinsertid += 1
                    newrow.colnumbers = self.colnumbers
                    newrow.colnames = self.colnames
                    newrow.setid(self.lastinsertid)
                    self.rows.append(newrow)
            return True
        elif type(rows) == type({}):
                newrow = ocfdbrow()
                newrow.cols = []
                newrow.colsa = {}
                
                for key in self.colnumbers:            
                    try:
                        rows[key] = str(rows[key])
                        rows[key] = rows[key].replace(";", "")
                        rows[key] = rows[key].replace("\n", '\\n')
                        newrow.__dict__[key] = str(rows[key])
                        
                        newrow.colsa[key] = str(rows[key])
                        newrow.cols.append(str(rows[key]))
                    except:
                        newrow.__dict__[key] = ''
                        newrow.colsa[key] = ''
                        newrow.cols.append('')
                self.lastinsertid += 1
                newrow.colnumbers = self.colnumbers
                newrow.colnames = self.colnames
                newrow.setid(self.getlastinsertid())
                self.rows.append(newrow)
                return True
        else:
                return False
    
    def deleterows(self, wql = "", limit = ""):
        """ Deletes rows, using WQL """
        if type(wql) == type("s"):
            rows = self.getrows(wql, limit)
            
            if rows == []:
                return False
            
            for row in rows:
                self.deleterows(int(row))
                
            return True
        elif type(wql) == type(1):                                            
                nrows = []
                
                for row in self.rows:
                    if row.getid() != wql:
                        nrows.append(row)
                
                self.rows = nrows
                return True
        else:
            return False

    

class ocfdbrow(ocfdbtable):
    """ Row Class
    You can access columns of row by attribute, for example
        print row.column1
    Changing values for row:
        row.column1 = value2
    """
    def __init__(self):
        pass
        
    def getrow(self, cols):
        """inserts values to column"""
        if self.__dict__.has_key("__changed"):
            self.__changed = False
            
        self.cols = cols
        pointer = 0
        self.colsa = {}
        for col in cols:
            
            self.colsa[self.colnumbers[pointer]] = col
            self.__dict__[self.colnumbers[pointer]] = col
            pointer += 1        
    
    def setid(self, id):
        """Sets row's internal ID"""
        self.__id = id

    def getid(self):
        """Returns row's internal ID"""
        return self.__id
    
    
    def returncsv(self):
        """returncsv is used while saving database"""
        strp =""
        
        if self.__changed == False:
            return self.cvs
        
        cols = [ self.__dict__[name] for name in self.colnumbers ]                
        
        if type(self.cols) == type([]):
            strp = ";".join(cols)
            return strp
        else:
            return str()
    
    def getcolumnbyname(self, colname):
        try:
            return self.colsa[colname]
        except:
            raise OCFDBError,  "Column "+colname+" doesn't exist!"
    
    
    
    def __setattr__(self, name, value):
        self.__dict__[name] = value
        self.__dict__['__changed'] = True
        self.__dict__['__dbtablechanged'] = True;
        
    #def __str__(self):
    #    return str(self.colsa)    
    
    def __getattr__(self, name):
            if self.colsa.has_key(name):
                return self.getcolumnbyname(name)    
            else:
                pass
